home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-9.10-netbook-remix-PL.iso / casper / filesystem.squashfs / usr / share / pyshared / epsilon / ampauth.py < prev    next >
Text File  |  2009-03-13  |  10KB  |  313 lines

  1. # -*- test-case-name: epsilon.test.test_ampauth -*-
  2. # Copyright (c) 2008 Divmod.  See LICENSE for details.
  3.  
  4. """
  5. This module provides integration between L{AMP<twisted.protocols.amp.AMP>} and
  6. L{cred<twisted.cred>}.
  7. """
  8.  
  9. from sha import sha
  10.  
  11. from zope.interface import implements
  12.  
  13. from twisted.python.randbytes import secureRandom
  14. from twisted.cred.error import UnauthorizedLogin
  15. from twisted.cred.credentials import IUsernameHashedPassword, IUsernamePassword
  16. from twisted.cred.checkers import ICredentialsChecker
  17. from twisted.protocols.amp import IBoxReceiver, String, Command, AMP
  18. from twisted.internet.protocol import ServerFactory
  19.  
  20. from epsilon.iepsilon import IOneTimePad
  21. from epsilon.structlike import record
  22.  
  23. __metaclass__ = type
  24.  
  25.  
  26. class UnhandledCredentials(Exception):
  27.     """
  28.     L{login} was passed a credentials object which did not provide a recognized
  29.     credentials interface.
  30.     """
  31.  
  32.  
  33.  
  34. class OTPLogin(Command):
  35.     """
  36.     Command to initiate a login attempt where a one-time pad is to be used in
  37.     place of username/password credentials.
  38.     """
  39.     arguments = [('pad', String())]
  40.  
  41.     errors = {
  42.         # Invalid username or password
  43.         UnauthorizedLogin: 'UNAUTHORIZED_LOGIN',
  44.         # No IBoxReceiver avatar
  45.         NotImplementedError: 'NOT_IMPLEMENTED_ERROR'}
  46.  
  47.  
  48.  
  49. class PasswordLogin(Command):
  50.     """
  51.     Command to initiate a username/password-based login attempt.  The response
  52.     to this command is a challenge which must be responded to based on the
  53.     correct password associated with the username given to this command.
  54.     """
  55.     arguments = [('username', String())]
  56.     response = [('challenge', String())]
  57.  
  58.  
  59.  
  60. def _calcResponse(challenge, nonce, password):
  61.     """
  62.     Compute the response to the given challenge.
  63.  
  64.     @type challenge: C{str}
  65.     @param challenge: An arbitrary byte string, probably received in response
  66.         to (or generated for) the L{PasswordLogin} command.
  67.  
  68.     @type nonce: C{str}
  69.     @param nonce: An arbitrary byte string, generated by the client to include
  70.         in the hash to avoid making the client an oracle.
  71.  
  72.     @type password: C{str}
  73.     @param password: The known correct password for the account being
  74.         authenticated.
  75.  
  76.     @rtype: C{str}
  77.     @return: A hash constructed from the three parameters.
  78.     """
  79.     return sha('%s %s %s' % (challenge, nonce, password)).digest()
  80.  
  81.  
  82.  
  83. class PasswordChallengeResponse(Command):
  84.     """
  85.     Command to respond to a challenge issued in the response to a
  86.     L{PasswordLogin} command and complete a username/password-based login
  87.     attempt.
  88.  
  89.     @param cnonce: A randomly generated string used only in this response.
  90.     @param response: The SHA-1 hash of the challenge, cnonce, and password.
  91.     """
  92.     arguments = [('cnonce', String()),
  93.                  ('response', String())]
  94.  
  95.     errors = {
  96.         # Invalid username or password
  97.         UnauthorizedLogin: 'UNAUTHORIZED_LOGIN',
  98.         # No IBoxReceiver avatar
  99.         NotImplementedError: 'NOT_IMPLEMENTED_ERROR'}
  100.  
  101.     @classmethod
  102.     def determineFrom(cls, challenge, password):
  103.         """
  104.         Create a nonce and use it, along with the given challenge and password,
  105.         to generate the parameters for a response.
  106.  
  107.         @return: A C{dict} suitable to be used as the keyword arguments when
  108.             calling this command.
  109.         """
  110.         nonce = secureRandom(16)
  111.         response = _calcResponse(challenge, nonce, password)
  112.         return dict(cnonce=nonce, response=response)
  113.  
  114.  
  115.  
  116. class _AMPUsernamePassword(record('username challenge nonce response')):
  117.     """
  118.     L{IUsernameHashedPassword} implementation used by L{PasswordLogin} and
  119.     related commands.
  120.     """
  121.     implements(IUsernameHashedPassword)
  122.  
  123.     def checkPassword(self, password):
  124.         """
  125.         Check the given plaintext password against the response in this
  126.         credentials object.
  127.  
  128.         @type password: C{str}
  129.         @param password: The known correct password associated with
  130.             C{self.username}.
  131.  
  132.         @return: A C{bool}, C{True} if this credentials object agrees with the
  133.             given password, C{False} otherwise.
  134.         """
  135.         if isinstance(password, unicode):
  136.             password = password.encode('utf-8')
  137.         correctResponse = _calcResponse(self.challenge, self.nonce, password)
  138.         return correctResponse == self.response
  139.  
  140.  
  141.  
  142. class _AMPOneTimePad(record('padValue')):
  143.     """
  144.     L{IOneTimePad} implementation used by L{OTPLogin}.
  145.  
  146.     @ivar padValue: The value of the one-time pad.
  147.     @type padValue: C{str}
  148.     """
  149.     implements(IOneTimePad)
  150.  
  151.  
  152.  
  153. class CredReceiver(AMP):
  154.     """
  155.     Integration between AMP and L{twisted.cred}.
  156.  
  157.     This implementation is limited to a single authentication per connection.
  158.     A future implementation may use I{routes} to allow multiple authentications
  159.     over the same connection.
  160.  
  161.     @ivar portal: The L{Portal} against which login will be performed.  This is
  162.         expected to be set by the factory which creates instances of this
  163.         class.
  164.  
  165.     @ivar logout: C{None} or a no-argument callable.  This is set to the logout
  166.         object returned by L{Portal.login} and is set while an avatar is logged
  167.         in.
  168.  
  169.     @ivar challenge: The C{str} which was sent as a challenge in response to
  170.         the L{PasswordLogin} command.  If multiple L{PasswordLogin} commands
  171.         are sent, this is the challenge sent in response to the most recent of
  172.         them.  It is not set before L{PasswordLogin} is received.
  173.  
  174.     @ivar username: The C{str} which was received for the I{username} parameter
  175.         of the L{PasswordLogin} command.  The lifetime is the same as that of
  176.         the I{challenge} attribute.
  177.     """
  178.     portal = None
  179.     logout = None
  180.  
  181.     @PasswordLogin.responder
  182.     def passwordLogin(self, username):
  183.         """
  184.         Generate a new challenge for the given username.
  185.         """
  186.         self.challenge = secureRandom(16)
  187.         self.username = username
  188.         return {'challenge': self.challenge}
  189.  
  190.  
  191.     def _login(self, credentials):
  192.         """
  193.         Actually login to our portal with the given credentials.
  194.         """
  195.         d = self.portal.login(credentials, None, IBoxReceiver)
  196.         def cbLoggedIn((interface, avatar, logout)):
  197.             self.logout = logout
  198.             self.boxReceiver = avatar
  199.             self.boxReceiver.startReceivingBoxes(self.boxSender)
  200.             return {}
  201.         d.addCallback(cbLoggedIn)
  202.         return d
  203.  
  204.  
  205.     @PasswordChallengeResponse.responder
  206.     def passwordChallengeResponse(self, cnonce, response):
  207.         """
  208.         Verify the response to a challenge.
  209.         """
  210.         return self._login(_AMPUsernamePassword(
  211.             self.username, self.challenge, cnonce, response))
  212.  
  213.  
  214.     @OTPLogin.responder
  215.     def otpLogin(self, pad):
  216.         """
  217.         Verify the given pad.
  218.         """
  219.         return self._login(_AMPOneTimePad(pad))
  220.  
  221.  
  222.     def connectionLost(self, reason):
  223.         """
  224.         If a login has happened, perform a logout.
  225.         """
  226.         AMP.connectionLost(self, reason)
  227.         if self.logout is not None:
  228.             self.logout()
  229.             self.boxReceiver = self.logout = None
  230.  
  231.  
  232.  
  233. class OneTimePadChecker(record('pads')):
  234.     """
  235.     Checker which validates one-time pads.
  236.  
  237.     @ivar pads: Mapping between valid one-time pads and avatar IDs.
  238.     @type pads: C{dict}
  239.     """
  240.     implements(ICredentialsChecker)
  241.  
  242.     credentialInterfaces = (IOneTimePad,)
  243.  
  244.     # ICredentialsChecker
  245.     def requestAvatarId(self, credentials):
  246.         if credentials.padValue in self.pads:
  247.             return self.pads.pop(credentials.padValue)
  248.         raise UnauthorizedLogin('Unknown one-time pad')
  249.  
  250.  
  251.  
  252. class CredAMPServerFactory(ServerFactory):
  253.     """
  254.     Server factory useful for creating L{CredReceiver} instances.
  255.  
  256.     This factory takes care of associating a L{Portal} with L{CredReceiver}
  257.     instances it creates.
  258.  
  259.     @ivar portal: The portal which will be used by L{CredReceiver} instances
  260.         created by this factory.
  261.     """
  262.     protocol = CredReceiver
  263.  
  264.     def __init__(self, portal):
  265.         self.portal = portal
  266.  
  267.  
  268.     def buildProtocol(self, addr):
  269.         proto = ServerFactory.buildProtocol(self, addr)
  270.         proto.portal = self.portal
  271.         return proto
  272.  
  273.  
  274.  
  275. def login(client, credentials):
  276.     """
  277.     Authenticate using the given L{AMP} instance.  The protocol must be
  278.     connected to a server with responders for L{PasswordLogin} and
  279.     L{PasswordChallengeResponse}.
  280.  
  281.     @param client: A connected L{AMP} instance which will be used to issue
  282.         authentication commands.
  283.  
  284.     @param credentials: An object providing L{IUsernamePassword} which will
  285.         be used to authenticate this connection to the server.
  286.  
  287.     @return: A L{Deferred} which fires when authentication has succeeded or
  288.         which fails with L{UnauthorizedLogin} if the server rejects the
  289.         authentication attempt.
  290.     """
  291.     if not IUsernamePassword.providedBy(credentials):
  292.         raise UnhandledCredentials()
  293.     d = client.callRemote(
  294.         PasswordLogin, username=credentials.username)
  295.     def cbChallenge(response):
  296.         args = PasswordChallengeResponse.determineFrom(
  297.             response['challenge'], credentials.password)
  298.         d = client.callRemote(PasswordChallengeResponse, **args)
  299.         return d.addCallback(lambda ignored: client)
  300.     d.addCallback(cbChallenge)
  301.     return d
  302.  
  303.  
  304.  
  305. __all__ = [
  306.     'UnhandledCredentials',
  307.  
  308.     'OTPLogin', 'OneTimePadChecker',
  309.  
  310.     'PasswordLogin', 'PasswordChallengeResponse', 'CredReceiver',
  311.  
  312.     'CredAMPServerFactory', 'login']
  313.